package ru.lukyanets.torrent.bencode;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author slukyanets
 */
public final class BDecoder {
    private final InputStream is;

    private BDecoder(InputStream is) {
        this.is = is;
    }

    public static Object decode(InputStream is) throws IOException {
        if (!is.markSupported()) { // Guarantees reset support
            is = new BufferedInputStream(is);
        }
        return new BDecoder(is).decodeObject();
    }

    private Object decodeObject() throws IOException {
        // remember stream position for byte array reading
        is.mark(Integer.MAX_VALUE);

        int code = is.read();
        Object element;
        if (Character.isDigit(code)) {
            is.reset();
            element = decodeBytes();
        } else if (code == BencodeConstants.PREFIX_NUMBER) {
            element = decodeNumber();
        } else if (code == BencodeConstants.PREFIX_LIST) {
            element = decodeList();
        } else if (code == BencodeConstants.PREFIX_MAP) {
            element = decodeMap();
        } else if (code == BencodeConstants.SUFFIX_END || code == -1){
            return null;
        } else {
            throw new IOException("Unknown element type: " + code);
        }

        return element;
    }

    private long decodeNumber() throws IOException {
        return decodeNumber(BencodeConstants.SUFFIX_END);
    }
    private long decodeNumber(char end) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        int bt = is.read();
        while (bt != end && bt != -1) {
            os.write(bt);
            bt = is.read();
        }

        return Long.parseLong(os.toString());
    }

    private byte[] decodeBytes() throws IOException {
        long len = decodeNumber(BencodeConstants.SPLITTER);
        if (len <= 0) {
            return null;
        } else if(len > Integer.MAX_VALUE) {
            throw new IOException("Byte array length too large: " + len);
        }

        int intLen = (int) len;
        byte[] tempArray = new byte[intLen];
        int count = 0;
        int rdLen;
        while (count != intLen && (rdLen = is.read(tempArray, count, intLen - count)) > 0) {
            count += rdLen;
        }

        return tempArray;
    }

    private List<?> decodeList() throws IOException {
        List<Object> list = new LinkedList<>();
        Object el;
        while ((el = decodeObject()) != null) {
            list.add(el);
        }
        return list;
    }

    private Map<String, Object> decodeMap() throws IOException {
        Map<String, Object> map = new HashMap<>();
        Object key;
        while ((key = decodeObject()) != null) {
            map.put(new String((byte[])key), decodeObject());
        }
        return map;
    }
}
